/////////////////////////////////////////////////////////////////
// Script:  slcicp menu script
//          - manages menus if the prim is the menu prim of the graph
//          - shows dialogs and handles user interactions
//
/////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////
// constants
/////////////////////////////////////////////////////////////////

string      MENU_LABEL_MORE     = ">>"; // "more"-button if there are more than 12 buttons
string      MENU_SEPARATOR      = "[NEW_MENU]"; // used to define start of a new menu definition more unambiguous
integer     MENU_MAX_BUTTONS    = 12; // maximum of buttons in a dialog (SL-Limitation)
integer     MENU_MORE_ID_OFFSET = 100; // offset of "more"-button identifiers (to separate them of "normal" buttons)

// constants for internal menu communication
integer     CMD_MENU            = 1; // prefix for specifying the message as menu communication
integer     CMD_ADD_MENU        = 2; // add a menu
integer     CMD_ADD_BUTTON      = 4; // add a button to last added menu
integer     CMD_SHOW_MENU       = 8; // display menu to user and handle user actions
integer     CMD_ADD_NODE        = 16; // add a node to selected nodes list
integer     CMD_REMOVE_NODE     = 32; // remove a node from nodes list
integer     CMD_MENU_CHOICE     = 64; // message contains menu choice by user and the list of selected nodes
integer        CMD_RESET            = 128; // reset script

/////////////////////////////////////////////////////////////////
// global vars
/////////////////////////////////////////////////////////////////

integer     graphId             = 0; // id of graph
list        graphMenuList       = []; // stores the menu definitions used in the graph (separated by MENU_SEPARATOR)
list        graphSelectedNodes  = []; // stores selected nodes in graph as a list of nodeIds
integer     menuChannel         = 0; // channel for menu dialogs
integer     menuSrcPrimId       = 0; // id of cicp prim that requested a menu dialog
string      menuSrcMenuId       = ""; // identifier of the menu requested by some cicp prim (see graphNodeMenuId)
integer     menuSrcNodeId       = 0; // node id of cicp prim that requested a menu dialog
string      menuSrcMessage      = ""; // menu message of the requested menu
list        menuSrcButtons      = []; // list of button labels of the current menu
key         menuSrcUserId       = ""; // user that requested the menu

/////////////////////////////////////////////////////////////////
// functions
/////////////////////////////////////////////////////////////////

// extracts neccessary informations out of menuParameters
// for displaying a menu
prepareMenuDialog(list menuParameters)
{
    integer length = llGetListLength(menuParameters);
    // store parameters to keep them for further processing
    menuSrcPrimId = llList2Integer(menuParameters, 0);
    menuSrcNodeId = llList2Integer(menuParameters, 1);
    menuSrcUserId = llList2Key(menuParameters, 2);
    menuSrcMessage = llUnescapeURL(llList2CSV(llList2List(menuParameters, 3, length - 3)));
    menuSrcMenuId = llList2String(menuParameters, length - 2);
    menuChannel = llList2Integer(menuParameters, length - 1);

    // show first button set of this menu
    changeButtonSet(0);
}

// displays a menu dialog with the menu menuSrcMenuId to the user menuSrcUserId
// buttonSet can be used to choose a specific 12-button-set (starting at 0)
// e.g. buttonSet = 2 would create a menu without the first 24 buttons
// use this parameter to workaround the 12 button limit of SL
changeButtonSet(integer buttonSet)
{
    // get button labels
    list buttons = llList2ListStrided(llDeleteSubList(getMenuButtons(menuSrcMenuId), 0, 0), buttonSet * MENU_MAX_BUTTONS, MENU_MAX_BUTTONS * 2 - 1, 2);
    if (buttons != []) {
        // extract buttonSet and show dialog
        menuSrcButtons = buttons;
    }
}

// displays dialog to user and sets a timeout for the interaction
displayMenuDialog()
{
    // show Dialog
    llDialog(menuSrcUserId, menuSrcMessage, menuSrcButtons, menuChannel);
    // set timeout for user interaction
    llSetTimerEvent(10.);
}

// processes menu interactions by user
processMenuChoice(string buttonLabel)
{
    // process normal menu choices
    integer menuOptionId = getButtonIdentifier(menuSrcMenuId, buttonLabel);
    // check for "more"-buttons in the menu dialog
    if (menuOptionId < -(MENU_MORE_ID_OFFSET - 1)) {
        // "more"-button was pressed
        // display next button set
        integer nextButtonSet = -(menuOptionId - MENU_MORE_ID_OFFSET - 1);
        changeButtonSet(nextButtonSet);
        displayMenuDialog();
    }
    else {
        // transmit menu choice and list of selected nodes to cicp hub
        string menuChoiceString = (string)menuSrcPrimId + "," + (string)graphId + "," + (string)menuSrcNodeId + "," + (string)menuOptionId;
        // add selected nodes if there are some
        if (llGetListLength(graphSelectedNodes) > 0) {
            menuChoiceString += "," + llDumpList2String(graphSelectedNodes, ";");
        }
        llMessageLinked(LINK_THIS, CMD_MENU | CMD_MENU_CHOICE, menuChoiceString, "");
    }
}

// returns identifier of last added menu
string getLastMenuIdentifier()
{
    integer entryCount = llGetListLength(graphMenuList) - 1;
    string lastEntry = "";
    string entry = "";
    // go backwards through menu list and look for first menu separator
    for (; entryCount > -1; --entryCount) {
        entry = llList2String(graphMenuList, entryCount);
        if (entry == MENU_SEPARATOR) {
            // this is a separator, so last entry had to be an identifier
            return lastEntry;
        }
        lastEntry = entry;
    }
    return "";
}

// add a menu identified by menuIdentifier to menu list
addMenu(string menuIdentifier)
{
    graphMenuList = graphMenuList + [MENU_SEPARATOR, menuIdentifier, ""];
}

// returns the option id of the option labelled with buttonLabel
// in the menu menuIdentifier
// returns false if either menu or button is  not defined
integer getButtonIdentifier(string menuIdentifier, string buttonLabel)
{
    list menuButtons = getMenuButtons(menuIdentifier);
    if (menuButtons != []) {
        // construct list of labels only to search in it for buttonLabel
        list buttonLabels = llList2ListStrided(llDeleteSubList(menuButtons, 0, 0), 0, -1, 2);
        integer buttonLabelPos = llListFindList(buttonLabels, [buttonLabel]);
        if (~buttonLabelPos) {
            // return buttonLabelPos in menuButtons to return its optionid
            return llList2Integer(menuButtons, buttonLabelPos << 1);
        }
    }
    return FALSE;
}

// returns the menu defined by menuIdentifier as a strided list of its buttons
// returns an empty list if not defined
list getMenuButtons(string menuIdentifier)
{
    integer menuButtonsEntryPos = getMenuEnd(menuIdentifier);
    list menuButtons = [];
    if (~menuButtonsEntryPos) {
        // menu found, extract menu buttons
        list buttons = llCSV2List(llList2String(graphMenuList, menuButtonsEntryPos));
        if (llGetListLength(buttons) > 1) {
            menuButtons = buttons;
        }
    }
    return menuButtons;
}

// returns position of first entry of the menu definiton identified by menuIdentifier
// note: this is the position of the MENU_SEPARATOR; to get the very first element
// (e.g. the menuIdentifier) increment the returned value by one.
integer getMenuStart(string menuIdentifier)
{
    return llListFindList(graphMenuList, [MENU_SEPARATOR, menuIdentifier]);
}

// returns position of last entry of the menu definition identified by menuIdentifier
// this is usually the buttons entry
integer getMenuEnd(string menuIdentifier)
{
    integer start = getMenuStart(menuIdentifier);
    if (~start) {
        ++start; // move after MENU_SEPARATOR
        // narrow selection to one menu (cut following menus)
        integer menuLastEntryPos = -1;
        integer nextMenuStart = llListFindList(llList2List(graphMenuList, start, -1), [MENU_SEPARATOR]);
        if (~nextMenuStart) {
            menuLastEntryPos = start + nextMenuStart - 1;
        }
        else {
            menuLastEntryPos = llGetListLength(graphMenuList) - 1;
        }
        return menuLastEntryPos;
    }
    else {
        return -1;
    }
}

// adds a button with identifier buttonIdentifier and label buttonLabel to
// the menu identified by menuIdentifier
// note: if menu is not defined, the function fails silently
addMenuButton(string menuIdentifier, integer buttonIdentifier, string buttonLabel)
{
    list menuButtons = getMenuButtons(menuIdentifier);
    if (~getMenuStart(menuIdentifier)) {
        // handle 12-button-limit with a "more"-button after 11 buttons
        integer buttonCount = llGetListLength(menuButtons) >> 1;
        if (buttonCount % MENU_MAX_BUTTONS == 0 && buttonCount != 0) {
            integer buttonSetCount = llFloor(buttonCount / MENU_MAX_BUTTONS);
            // add "more"-button at the right bottom (=at position 2 of last button set)
            integer moreButtonPosition = -(MENU_MAX_BUTTONS << 1); // = -24
            menuButtons = llList2List(menuButtons, 0, moreButtonPosition - 1)
            + [-(buttonSetCount + MENU_MORE_ID_OFFSET), MENU_LABEL_MORE + " " + (string)buttonSetCount]
            + llList2List(menuButtons, moreButtonPosition, -1);
        }

        // add button to list
        menuButtons = menuButtons + [buttonIdentifier, buttonLabel];

        // overwrite old with new, extended button list
        integer buttonEntryPos = getMenuEnd(menuIdentifier);
        graphMenuList = llListReplaceList(graphMenuList, [llList2CSV(menuButtons)], buttonEntryPos, buttonEntryPos);
    }
}

// add node nodeId to list of selected nodes
addToSelected(integer nodeId) {
    // add if not in list yet
    if (llListFindList(graphSelectedNodes, [nodeId]) == -1) {
        graphSelectedNodes = [nodeId] + graphSelectedNodes;
    }
}

// remove node nodeId from list of selected nodes
removeFromSelected(integer nodeId) {
    integer nodePos = llListFindList(graphSelectedNodes, [nodeId]);
    if (~nodePos) {
        graphSelectedNodes = llDeleteSubList(graphSelectedNodes, nodePos, nodePos);
    }
}

/////////////////////////////////////////////////////////////////
//  states: default, menu
/////////////////////////////////////////////////////////////////

 default
 {
     link_message(integer linkNumber, integer number, string data, key id)
     {
         if (number & CMD_MENU) {
             list parameters = llCSV2List(data);
             if (number & CMD_ADD_MENU) {
                 graphId = llList2Integer(parameters, 1);
                 addMenu(llList2String(parameters, 0));
             }
             else if (number & CMD_ADD_BUTTON) {
                 addMenuButton(getLastMenuIdentifier(), llList2Integer(parameters, 0), llList2String(parameters, 1));
             }
             else if (number & CMD_SHOW_MENU) {
                 prepareMenuDialog(parameters);
                 state menu;
             }
             else if (number & CMD_ADD_NODE) {
                 addToSelected((integer)data);
             }
             else if (number & CMD_REMOVE_NODE) {
                 removeFromSelected((integer)data);
             }
             else if (number & CMD_RESET) {
                 llResetScript();
             }
         }
     }
 }

state menu
// prim displays menus ("is a menu prim")
{
    state_entry()
    {
        llListen(menuChannel, "", menuSrcUserId, "");
        displayMenuDialog();
    }

    listen(integer channel, string name, key id, string msg)
    {
        if (channel == menuChannel) {
            processMenuChoice(msg);
        }
    }

    touch_start(integer n)
    {
        // re-display dialog when user touches prim
        if (llDetectedKey(0) == menuSrcUserId) {
            displayMenuDialog();
        }
    }

    timer()
    {
        // change (back) to graphing state
        state default;
    }

    state_exit()
    {
        // reset timer
        llSetTimerEvent(0.);
    }

      link_message(integer linkNumber, integer number, string data, key id)
     {
         if (number & CMD_MENU) {
             if (number & CMD_ADD_NODE) {
                 addToSelected((integer)data);
             }
             else if (number & CMD_REMOVE_NODE) {
                 removeFromSelected((integer)data);
             }
             else if (number & CMD_RESET) {
                 llResetScript();
             }
         }
     }
}
